Skip to content

feat(nextjs): Add @sentry/nextjs/cloudflare - #24998

Open
JPeer264 wants to merge 4 commits into
developfrom
jp/nextjs-cloudflare-entry
Open

JPeer264 wants to merge 4 commits into
developfrom
jp/nextjs-cloudflare-entry

Conversation

@JPeer264

@JPeer264 JPeer264 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

(disclaimer, most of the additions are from withSentry.test.ts and other tests)

Adds withSentry from @sentry/nextjs/cloudflare for the Worker entry of a Next.js app on Cloudflare Workers, for example .open-next/worker.js of OpenNext or the fetch handler of vinext. It is withSentry of @sentry/cloudflare with the Next.js handling of the server init added, so a Next.js app on Workers gets the spans it gets on Node.js from one wrapper, without new options. This is the only new public API.

Decisions:

  • It installs the OpenTelemetry async context strategy at module load. The AsyncLocalStorage strategy of @sentry/cloudflare loses the OpenTelemetry context of the Next.js spans, so their parents and context values break.
  • The request spans of Next.js (BaseServer.handleRequest) are ignored, so the request span of withSentry is the only http.server span. The other Next.js spans become its children; it gets the route from their next.route and its status from the response. When the middleware answers the request or throws, the span is named middleware GET, like the middleware segment on Node.js.
  • From compatibility_date 2026-02-19, process.cwd() is /bundle during a request. Next.js then misses its router server context and extracts the incoming trace again. The propagator keeps the request span as parent in that case, else each continued request has two segments.
  • sentry.server.config.ts and sentry.edge.config.ts still run in the Worker but create no client there. Their options do not apply, and a debug log says so. On Workers they are optional: they only set the turbopack tag and hand over the build release of withSentryConfig, which only code that Next.js compiles can read. Without them, the release comes from the withSentry options, SENTRY_RELEASE or CF_VERSION_METADATA. Apps keep them for next dev, which runs on Node.js and does not use the Worker entry. instrumentation.ts stays required for onRequestError.
  • The Next.js handling is added by a Nextjs integration. withSentry of @sentry/cloudflare creates the client itself (once per isolate), and the options callback only returns options. The setup of an integration is the only place that gets this client before the request span starts, without a new option in @sentry/cloudflare. It registers the span hooks of the server init, the propagator, the tunnel route sampling and the event processor that drops the control flow errors of React and Next.js, so a Worker without instrumentation.ts drops them too. The server init checks for this integration, so it does not add the processor to the global scope again.
  • @sentry/cloudflare becomes a dependency of @sentry/nextjs. Its dependencies are already in the dependency tree of @sentry/nextjs. It also aligns with other SDKs, such as @sentry/sveltekit

Known difference: on Workers, a release from the withSentry options, SENTRY_RELEASE or CF_VERSION_METADATA wins over the build release. On Node.js, the build release wins over the environment.

The nextjs-16-cf-workers e2e app now uses it, which un-skips its server tests and adds tests for D1 spans, the OpenTelemetry context and trace continuation.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.72 kB - -
@sentry/browser - with treeshaking flags 27.86 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.75 kB - -
@sentry/browser (incl. Tracing) 51.75 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 51.75 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.73 kB - -
@sentry/browser (incl. Tracing, Replay) 91.44 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.34 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.14 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.15 kB - -
@sentry/browser (incl. Feedback) 47.24 kB - -
@sentry/browser (incl. sendFeedback) 34.77 kB - -
@sentry/browser (incl. FeedbackAsync) 39.85 kB - -
@sentry/browser (incl. Metrics) 30.72 kB - -
@sentry/browser (incl. Logs) 31.01 kB - -
@sentry/browser (incl. Metrics & Logs) 31.67 kB - -
@sentry/react 31.54 kB - -
@sentry/react (incl. Tracing) 54.07 kB - -
@sentry/vue 37.75 kB - -
@sentry/vue (incl. Tracing) 54.67 kB - -
@sentry/svelte 29.74 kB - -
@sentry/remix (Remix 3 client bundle) 56.76 kB - -
CDN Bundle 31.43 kB - -
CDN Bundle (incl. Tracing) 52.28 kB - -
CDN Bundle (incl. Logs, Metrics) 33.64 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.23 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.53 kB - -
CDN Bundle (incl. Tracing, Replay) 89.91 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.86 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.06 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.05 kB - -
CDN Bundle - uncompressed 92.72 kB - -
CDN Bundle (incl. Tracing) - uncompressed 155.25 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.25 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.21 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.22 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 275.35 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 281.29 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.05 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 294.98 kB - -
@sentry/nextjs (client) 56.45 kB - -
@sentry/sveltekit (client) 52.14 kB - -
@sentry/core/server 40.85 kB +0.01% +1 B 🔺
@sentry/core/browser 13.71 kB - -
@sentry/node 145.87 kB +0.01% +8 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.33 kB - -
@sentry/node - without tracing 93.66 kB +0.02% +10 B 🔺
@sentry/node - without channel injection 124.01 kB +0.02% +13 B 🔺
@sentry/aws-serverless 101.88 kB -0.01% -4 B 🔽
@sentry/cloudflare (withSentry) - minified 209.67 kB -0.01% -3 B 🔽
@sentry/cloudflare (withSentry) 519.89 kB -0.01% -3 B 🔽
@sentry/nextjs/cloudflare (withSentry) - minified 227.33 kB added added

View base workflow run

@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch 2 times, most recently from fb54ebb to fda7a91 Compare October 2, 2026 19:29
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from fda7a91 to 1f9ac0a Compare October 3, 2026 16:14
@JPeer264
JPeer264 added this pull request to stack #25035 October 5, 2026 08:43
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from 1f9ac0a to 752d9d0 Compare October 5, 2026 09:13
@JPeer264

JPeer264 commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 752d9d0. Configure here.

@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch 2 times, most recently from b565598 to 9ee53ad Compare October 6, 2026 06:42
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch 3 times, most recently from ed04155 to c78e44c Compare October 6, 2026 12:57
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from c78e44c to 81692f3 Compare October 6, 2026 13:51
Base automatically changed from jp/nextjs-move-server-span-hooks to develop October 6, 2026 14:05
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from 81692f3 to 33d45ee Compare October 6, 2026 14:05
@JPeer264 JPeer264 self-assigned this Oct 6, 2026
@JPeer264
JPeer264 marked this pull request as ready for review October 6, 2026 14:21
@JPeer264
JPeer264 requested a review from a team as a code owner October 6, 2026 14:21
@JPeer264
JPeer264 requested review from chargome and s1gr1d and removed request for a team October 6, 2026 14:21
Comment thread packages/nextjs/src/server/index.ts Outdated
Comment thread packages/nextjs/src/server/serverSpanHooks.ts
Comment thread packages/nextjs/src/server/serverSpanHooks.ts
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from 7cbbd91 to d24902e Compare October 7, 2026 16:26
import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from '../server/serverSpanHooks';
import { nextjsUseCacheIntegration } from '../server/useCacheInstrumentation';

export * from '@sentry/cloudflare';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Does this one include an init? Might lead to some confusion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloudflare doesn't have init, so it should be good

}
}

const nextjsIntegration = (): Integration => ({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't tell if that is a good wow or bad wow :D

];
const { tracesSampler } = options;
if (tracesSampler) {
// A `tracesSampler` can ignore the `parentSampled: false` that the Next.js integration sets for tunnel requests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to actually have e2e coverage on the tunnelRoute feature (but this requires a new app)

Comment on lines +121 to +130
// on Node.js. A 404 page still names the root span.
const isErrorPageBehindMiddleware =
NEXTJS_ERROR_PAGE_ROUTES.includes(route) && rootSpanAttributes?.[SENTRY_SEGMENT_NAME_SOURCE] === 'route';
// eslint-disable-next-line typescript/no-deprecated
const method = rootSpanAttributes?.[HTTP_REQUEST_METHOD] || rootSpanAttributes?.[HTTP_METHOD];

// Only hoist the http.route attribute if the transaction doesn't already have it
if (!method || rootSpanAttributes?.[HTTP_ROUTE] || isErrorPageBehindMiddleware) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The isErrorPageBehindMiddleware check is too broad, causing it to incorrectly block error page route hoisting when a route handler, not middleware, has previously named the root span.
Severity: LOW

Suggested Fix

The condition should be more specific to only block hoisting when middleware has named the span. This could be achieved by setting a unique attribute on the root span within handleMiddlewareSpanStart that can be checked for, instead of relying on the shared SENTRY_SEGMENT_NAME_SOURCE === 'route' attribute.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/nextjs/src/server/serverSpanHooks.ts#L121-L130

Potential issue: In the Edge runtime, the condition to prevent hoisting an error page's
route to the root span is overly broad. The check `isErrorPageBehindMiddleware` uses
`SENTRY_SEGMENT_NAME_SOURCE === 'route'` to determine if middleware has already named
the span. However, route handlers can also set this attribute. If a route handler runs,
sets this attribute, and then an error occurs that renders an error page, this logic
will incorrectly prevent the error page's route (e.g., '/500') from being hoisted to the
transaction name. The transaction will incorrectly retain the name of the route handler
that failed, not the error page that was served.

JPeer264 and others added 4 commits October 8, 2026 10:46
`withSentry` from `@sentry/nextjs/cloudflare` wraps the Worker entry of a
Next.js app on Cloudflare Workers, e.g. `.open-next/worker.js` of OpenNext or
the fetch handler of vinext. It is `withSentry` of `@sentry/cloudflare` with
the Next.js handling added:

- It installs the OpenTelemetry async context strategy and context manager
  at module load, so the spans of Next.js nest and keep their OpenTelemetry
  context as on Node.js.
- Its client gets the span hooks and `ignoreSpans` of the server `init`, the
  event processor for the control flow errors of React, the `use cache`
  integration, the Sentry propagator and the Next.js SDK metadata.
- The propagator keeps the request span as parent when Next.js extracts an
  incoming trace that the root span already continued. Next.js does this
  when it misses its router server context, e.g. on Workers where
  `process.cwd()` is `/bundle`, and each continued request then had two
  segments.
- The request spans of Next.js (`BaseServer.handleRequest`) are ignored, so
  the request span of `withSentry` is the only `http.server` span. The other
  Next.js spans become its children; it gets the route from their
  `next.route` and its status from the response. When the middleware answers
  the request or throws, the span is named `middleware GET`, like the
  middleware segment on Node.js.
- Requests to the tunnel route are not sampled.
- The server and edge `init` in `sentry.*.config.ts` create no client in the
  Worker. They hand over the build release of `withSentryConfig`, which only
  code that Next.js compiles can read.

The `nextjs-16-cf-workers` e2e app now uses it. This runs the server tests
that were skipped, and adds tests for D1 spans, the OpenTelemetry context and
trace continuation. Its `compatibility_date` moves to 2026-02-19, the first
date on which Next.js extracts the incoming trace again.

A size-limit entry measures `withSentry` of the new entry with the build
settings of wrangler. An import from `@sentry/node` or the server `init`
module exceeds its limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JPeer264
JPeer264 force-pushed the jp/nextjs-cloudflare-entry branch from 0aa1a8f to f930202 Compare October 8, 2026 08:46
Comment on lines +38 to +42
// On Cloudflare Workers, the `http.server` span of `withSentry` from `@sentry/cloudflare` is the request root span.
if (
attributes[ATTR_NEXT_SPAN_TYPE] !== 'BaseServer.handleRequest' &&
attributes[SENTRY_ORIGIN] !== 'auto.http.cloudflare'
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: For middleware requests on Cloudflare, the root span's op is incorrectly set to 'http.server' instead of 'middleware' because the ATTR_NEXT_SPAN_NAME attribute is missing.
Severity: MEDIUM

Suggested Fix

In serverSpanHooks.ts, within the handleMiddlewareSpanStart function, set the ATTR_NEXT_SPAN_NAME attribute on the root span for the Cloudflare middleware execution path. This will align its behavior with the Node.js path, allowing the logic in enhanceHandleRequestRootSpan to correctly identify the request and set the transaction op to 'middleware'.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/nextjs/src/server/enhanceHandleRequestRootSpan.ts#L38-L42

Potential issue: For middleware-handled requests in a Cloudflare environment, the root
span's transaction `op` is incorrectly set to `'http.server'` instead of the expected
`'middleware'`. This occurs because the logic in `handleMiddlewareSpanStart` does not
set the `ATTR_NEXT_SPAN_NAME` attribute on the root span for Cloudflare. Consequently,
the logic in `enhanceHandleRequestRootSpan`, which relies on this attribute to override
the `op`, fails to identify it as a middleware request. This results in inconsistent
transaction classification between Node.js and Cloudflare deployments, potentially
affecting performance monitoring and filtering in Sentry.

Also affects:

  • packages/nextjs/src/server/serverSpanHooks.ts:158~168

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants